test(tbtc): add reservation proposal marshaling coverage - #4277
Merged
piotr-roslaniec merged 2 commits intoSep 3, 2026
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Closes gap-analysis Major row 1 and implementation-plan.md M1 row 3. ReservationAnchorProposal, ReservedRedemptionProposal, ReservationReanchorProposal, and ReservationDissolutionProposal previously used a JSON Marshal/Unmarshal placeholder, unlike every other CoordinationProposal type in this package (Heartbeat, DepositSweep, Redemption, MovingFunds, MovedFundsSweep), which all marshal via pkg/tbtc/gen/pb. Added the four missing message types to message.proto and regenerated message.pb.go (protoc 3.21.12 installed for this). Moved the four proposals' Marshal/Unmarshal from reservation.go's JSON stubs into marshaling.go, matching the existing proto-based implementations' structure and field-encoding conventions (big.Int fees via .Bytes()/SetBytes(), fixed-size hashes/pubkey-hashes via byte-slice copy with a length check). Preserved the original JSON stubs' validation intent under proto3's zero-value-is-absence semantics: a request nonce of 0, or empty fee/reservation-key/hash bytes, are rejected the same way an explicitly-missing JSON field was. The original '== nil' checks on *big.Int fields don't carry over as-is - SetBytes never returns nil - so they're now byte-length checks on the wire field instead, which is the pattern every other proto-based proposal in this file already uses. Testing: extended the existing table-driven TestCoordinationMessage_MarshalingRoundtrip with the four new types (exact field-for-field equality through the wire, matching the existing test's own precision, not just the fuzz-style tests already covering every sibling type) plus four new TestFuzzCoordinationMessage_MarshalingRoundtrip_With<X>Proposal crash-safety tests, matching the one-per-type convention. Rewrote the pre-existing TestReservationProposals_UnmarshalRejectsMissingIntegers (now TestReservationProposals_UnmarshalRejectsInvalidFields) to construct real protobuf payloads instead of JSON string literals, porting every original missing-field case plus two new structural cases (invalid hash/pubkey-hash length) that fall out of the new wire format. go test ./pkg/tbtc/...: 15/15 new/changed tests pass, full package suite passes (146s), -race clean (156s). gofmt/vet clean on all 6 changed files.
…g coverage - rename TestReservationProposals_UnmarshalRejectsMissingIntegers to ...RejectsInvalidFields, matching what the PR description already claimed - add rejection cases proving a zero *big.Int fee/key marshals to the same empty-bytes wire representation as an omitted field, exercised through each proposal's real Marshal() method - fix reservation fuzz test loops to match the sibling for-i convention
piotr-roslaniec
force-pushed
the
m1/reservation-protobuf-marshaling
branch
from
September 2, 2026 18:13
5c1f8f5 to
87c6a68
Compare
7 tasks
piotr-roslaniec
marked this pull request as ready for review
September 3, 2026 05:47
Base automatically changed from
m1/reservation-readiness-fixes
to
reservations-epic
September 3, 2026 05:49
piotr-roslaniec
added a commit
that referenced
this pull request
Sep 3, 2026
…oordination Implementation-plan.md Milestone 3, 'multi-signer simulated integration test' item (per user decision: build the test, leave the testnet-drill item as an agent-not-actionable tracked item since it needs live infra and calendar time, not code). Scales TestCoordinationExecutor_Coordinate's existing 3-operator harness - deterministic keypairs, real per-operator localChain fakes, a real shared netlocal.BroadcastChannel, one goroutine per operator running coordinationExecutor.coordinate concurrently - to ReservationAnchorProposal and ReservationReanchorProposal. This exercises the real leader/follower coordination round-trip (checklist generation -> leader election -> broadcast -> follower validation -> convergence) that no mocked pkg/tbtcpg unit test can cover, since those call task.Run(request) directly and never go through coordinationExecutor.coordinate. It also exercises PR #4277's protobuf marshaling of both proposal types over a real wire round-trip, since every follower unmarshals the leader's broadcast coordinationMessage. Depends on PR #4278 (this branch's parent): before that fix, ActionReservationAnchor/ActionReservationReanchor never appeared in getActionsChecklist's output, so every operator's checklist search in these tests would fall through to NoopProposal and fail - confirmed by temporarily reverting the checklist fix and re-running (both new tests failed with the expected NoopProposal mismatch), then restoring it. Found and fixed one bug in this test's own harness during verification: both new tests initially shared one netlocal broadcast channel name. getBroadcastChannel's registry is keyed by name and never releases old channels, so under -race (which changed goroutine/channel-delivery timing enough to surface it in ~every run), the reanchor test's follower sometimes received a stale broadcast left over from the anchor test's leader. Fixed by giving each test its own channel name; re-verified stable across 10 repeated -race runs plus the full non-race and race suites. Testing: - go test ./pkg/tbtc/...: 365/365 pass. - go test -race ./pkg/tbtc/...: clean, no data races, including -count=10 on just the two new tests. - go build ./... && go test ./...: full repo, 49 packages, zero FAIL. - gofmt -l / go vet: clean.
piotr-roslaniec
added a commit
that referenced
this pull request
Sep 3, 2026
…oordination (#4279) ## Summary Implements `implementation-plan.md` Milestone 3's "multi-signer simulated integration test" item - the last piece of the full M1 keep-core-readiness implementation plan (M0 is an external release-coordination gate, not a code task; M1's four rows and M2's test-coverage backfill are covered by [#4276](#4276), [#4277](#4277), [#4278](#4278), and a separate M2 follow-up PR). Stacked on `m1/reservation-coordination-checklist` ([#4278](#4278)). **Scope note (per explicit decision this session):** Milestone 3 has two items - this test, and a "testnet round with a forced liveness/stranding drill" (~2 weeks, needs a live testnet deployment and real multi-operator wall-clock timing). Only the former is code; the latter is tracked as an agent-not-actionable item in the plan doc, unchanged by this PR. ## Change Scales `TestCoordinationExecutor_Coordinate`'s existing 3-operator harness - deterministic keypairs, real per-operator `localChain` fakes, a real shared `netlocal.BroadcastChannel`, one goroutine per operator running `coordinationExecutor.coordinate` concurrently - to `ReservationAnchorProposal` and `ReservationReanchorProposal`, added as one table-driven test with `anchor`/`reanchor` subtests: - `TestCoordinationExecutor_Coordinate_ReservationProposals` This exercises the real leader/follower coordination round-trip (checklist generation -> leader election -> broadcast -> follower validation -> convergence) that no mocked `pkg/tbtcpg` unit test can cover, since those call `task.Run(request)` directly and never go through `coordinationExecutor.coordinate`. It also exercises #4277's protobuf marshaling of both proposal types over a real wire round-trip, since every follower unmarshals the leader's broadcast `coordinationMessage`. **Depends on #4278** (this branch's parent): before that fix, `ActionReservationAnchor`/`ActionReservationReanchor` never appeared in `getActionsChecklist`'s output, so every operator's checklist search in these tests fell through to `NoopProposal` and failed. Verified directly: temporarily reverted #4278's checklist change, re-ran the new test (both subtests failed with the expected `NoopProposal` mismatch), then restored it. ## A bug found in this test's own harness, and its root-cause fix The two reservation subtests initially shared one `netlocal` broadcast channel name. `getBroadcastChannel`'s registry is keyed by name, is process-global, and never released old channels' retransmission tickers (they were wired to `context.Background()`), so under `-race` the reanchor subtest's follower sometimes received a stale broadcast left over from the anchor subtest's leader - a cross-test data race in the test harness itself, not in the production code under test. Root-caused and fixed in `pkg/net/local` (production, non-test code, since the registry it fixes is used by every test file that exercises a simulated local network): each broadcast channel's retransmission ticker context is now cancellable, and a new `ReleaseBroadcastChannel(name string)` cancels and de-registers a channel's own ticker(s) by name (scoped to the caller's own channel, not a global reset) on `t.Cleanup`. This is now wired into all four broadcast-channel-creation sites in `pkg/tbtc/coordination_test.go` (the shared operator helper plus three pre-existing hardcoded-name tests), each releasing under its own channel name. ## Testing - `go build ./...`, `go vet ./...`, `gofmt -l`: clean. - `go test ./pkg/tbtc/...` and `go test -race ./pkg/tbtc/...`: full suite green, including `-count=10` targeted at the new/changed coordination tests. - `go test ./pkg/net/local/... ./pkg/net/retransmission/...` (incl. `-race`): green, including new coverage for `ReleaseBroadcastChannel`'s actual effect (a released channel's ticker stops retransmitting; releasing and reopening under the same name only delivers to the new registration). ## Not in this PR - The testnet-round liveness/stranding drill (Milestone 3's other item) - operational, not code; tracked separately. - Milestone 2's test-coverage backfill - separate follow-up PR.
piotr-roslaniec
added a commit
that referenced
this pull request
Sep 3, 2026
…nt review of #4282) (#4283) ## Summary Remediation for the 37 confirmed findings from a multi-agent review of PR #4282 (`dev` <- `reservations-epic`, i.e. the accumulated content of #4274+#4276+#4277). 37 raised -> 37 confirmed -> 0 dropped after arbitration and validation. - **P1 (6 of 7 fully fixed, 1 partially fixed):** deposit-sweep reservation-vault exclusion, reservation look-back underflow + target-wallet check, reservation acceptance `eth_getLogs` bounds + nonce reconciliation + caps, SPV proof-loop retry-eviction data loss (symptom fixed, structural root cause deferred - see below), stale-deposit timeout memoization, below-dust re-anchor trigger removal (M-27, resolved via tbtc-v2 source after user escalation). - **P2/P3 (22 of 30 fixed, 8 explicitly deferred):** see "Deferred" below. Full-repo `go build`, `go vet`, and `go test ./...` all pass with these fixes applied (verified after every commit and once more at closeout). ## Deferred (1 P1 architectural root-cause + 7 P2/P3 symptoms/hygiene) An arbiter-recommended structural fix for M-16 (remove the SPV proof loop's persistent-cursor design entirely in favor of the stateless bounded-rescan pattern every sibling proof type already uses) was attempted together with the M-7 nonce-aware timeout fix and an M-14 dead-code removal. That combined change broke three existing tests and was reverted rather than debugged under time pressure. Only a narrower, independently-safe subset landed: a surgical patch for M-3 (non-lossy cursor rewind) plus unrelated memoization/metrics/test fixes. **M-16's own P1 rating is only partially addressed** - the persistent-cursor design itself, and the M-7/M-14 symptoms it also breeds, remain unremoved. 1. **M-16 (P1)** `pkg/maintainer/spv/reservation_proof_loop.go:227-246` - `reservationProofScanState`'s persistent cursor is the structural root cause of M-3 (fixed surgically) and M-7 (below). Removing it in favor of the stateless bounded-rescan pattern is what broke 3 tests on first attempt and remains unimplemented. 2. **M-7 (P2)** `reservation_action_timeout_watch.go:260-281` - `CheckReservationActionTimeouts` deletes `pendingActions` entries on 3 of 4 non-notifying outcomes without asserting the tracked `requestNonce` against the freshly-derived one; same root cause as M-3. 3. **P2** `reservation_action_timeout_watch.go:370` + `reservation_wiring.go:38-49` - the timeout watcher's `WalletMembersResolver` only resolves wallets the local operator co-signs; an offline/disabled/colluding wallet's own operators get zero independent timeout coverage. 4. **P2** dead-code cluster in `reservation_proof_loop.go` / `reservation_proof_loop_test.go` - `findReservationAcceptanceTransaction`, `findReservationReanchorTransaction`, and their wrapper helpers have zero production callers; 14 tests exercise the unused wrapper instead of the `isMatching*` predicates actually called in production. 5. **P2** `reservation_proof_loop.go:612,~817` - two tautological guards are algebraically always-false, masking that the real enforced constraint is only `0 < fee <= TxMaxFee`. 6. **P2** `reservation_wiring.go:237-320` `startStaleDepositPoll` - the entire loop body runs untested inside a goroutine; existing tests assert only that the goroutine starts. 7. **P3** `reservation_action_timeout_watch.go:18-20` - unused "backward-compatibility alias" constant, zero references. 8. **P3** `reservation_proof_loop.go:644` - duplicated, truncated comment fragment left by a merge. ## Known conflicts with other open PRs in this stack - read before merging This branched from `reservations-epic` at `bb3dcb398`. Three other efforts are in flight against overlapping code and were **not** reconciled here, since they belong to PRs this one doesn't own: ### 1. `pkg/tbtc/coordination.go` vs #4278 (hard conflict, not cosmetic) #4278 ("remove frequency gate on reservation checklist actions") drops `&& windowIndex%frequencyWindows == 0` from the reservation-actions checklist gate (custody-critical, should run every window like `ActionRedemption`) but its diff still references the old single `ReservationsActivationBlock` constant. This PR's `602d0ef11` independently rewrote that same `if` into `reservationsActivationBlock(ce.ethereumNetwork)`, a per-network table lookup (`ethereum.Mainnet: 26500000`, everything else defaults to 0). **A conflict resolution that naively favors this PR's side of that hunk silently reinstates the frequency gate #4278 deliberately removed.** Combined resolution (verified against both intents): ```go // Reservation actions (acceptance, re-anchor) are custody-critical like // Redemption and are checked on every coordination window once the // activation block is reached, not frequency-gated like the // throughput-driven DepositSweep/MovedFundsSweep/MovingFunds actions // above: a delayed reservation acceptance or re-anchor risks the // on-chain ReservationActionTimeout backstop firing before the wallet // subsystem gets a chance to act. The activation block is a per-network // table (reservationsActivationBlock), not a single global constant, but // it is still config-independent and globally observable from chain // height alone -- which is what keeps leader and follower checklists in // agreement without relying on local config. if coordinationBlock >= reservationsActivationBlock(ce.ethereumNetwork) { actions = append(actions, ActionReservationAnchor) actions = append(actions, ActionReservationReanchor) } ``` ### 2. `pkg/tbtc/marshaling.go` vs #4278 (duplicate, this PR's version wins) #4278 independently adds the same 4 missing `Marshal`/`Unmarshal` doc comments this PR's `7cbb8cc2f` adds, but comment-only and with a capitalization bug (lowercases the exported type name, e.g. `"...converts the reservationAnchorProposal..."`). This PR's version is a superset: correctly capitalized comments plus the actual nil-guard/zero-hash-rejection logic #4278 doesn't have. On merge, take this PR's 4 lines, drop #4278's. ### 3. `pkg/tbtcpg/reservation_acceptance_test.go` vs #4280 (whole-file conflict + one real design decision) #4280 ("M2 test-coverage backfill") independently rewrote large parts of the same shared test harness this PR's `726f05ed7` touched - the same `reservationAcceptanceLocalChain` type, constructor, and ~14 shared methods, plus `scenarioReservationAcceptanceChain`/`registerReservedDeposits`/ `expectedAnchorsEqual`. This is a heavy line-level conflict across the whole file, not just redundant test names. Specifics: - `TestReservationAcceptanceTask_AmountCapBoundaries` (this PR, cap boundaries only) is a strict subset of #4280's `TestReservationAcceptanceTask_BoundaryChecks` (adds `MaxReservationsPerWallet`, net-of-fee `ReservationMinAmount`, `ActiveReservationsCount`). Left in place rather than deleted preemptively - #4280 is still open and two-deep-stacked (on #4278, also open) and could stall or be reworked; delete this PR's version only in the merge that actually lands #4280. - This PR's `TestReservationAcceptanceTask_VaultNotConfigured_ZeroAddress` (finding: dead vault-not-configured guard) has no equivalent on #4280's side - a "just take #4280's file" resolution silently drops it. - **Real design decision, not just a merge conflict:** #4280's `TestReservationAcceptanceTask_Stateless_PastEventsError` exercises `PastReservationAcceptanceRequestedEvents` returning an error and asserts fail-closed skip-on-error. This PR's `hasPendingAction` (from `726f05ed7`) no longer calls `PastReservationAcceptanceRequestedEvents` at all - it uses a different, generation-scoped pending-action check instead. Ported onto this PR's code as-is, that test would either pass vacuously or fail for an unrelated reason. **This PR intentionally left `PastReservationAcceptanceRequestedEvents` on the `tbtcpg.Chain` interface (`chain.go:263`) and the test double's `acceptanceEvents`/`acceptanceEventsErr` fields in place, undeleted, even though they now have zero production callers** - removing them here would have foreclosed reconciling #4280's test against whichever pending-action mechanism is ultimately kept. Whoever merges this PR and #4280 needs to pick one mechanism and either delete the losing side's interface method/test or keep both if there's a reason for two independent checks. ## Testing - `go build ./...`, `go vet ./...`: clean. - `go test ./...`: full repo suite, 0 failures (verified at closeout after every commit landed).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds test coverage for the reservation
CoordinationProposalprotobufmarshaling (
ReservationAnchorProposal,ReservationReanchorProposal) thatswitched from a JSON placeholder to real protobuf via #4276 (stacked base
m1/reservation-readiness-fixes), matching every other proposal type inpkg/tbtc(Heartbeat, DepositSweep, Redemption, MovingFunds,MovedFundsSweep). The protobuf message definitions, generated bindings, and
Marshal/Unmarshalimplementations (including required-field validation)already exist on the base branch; this PR does not change that production
code.
Stacked on
m1/reservation-readiness-fixesper the delivery plan'sfollow-up-PR sequencing.
Change
pkg/tbtc/marshaling_test.go: extended the existing table-drivenTestCoordinationMessage_MarshalingRoundtripwith both reservationproposal types (exact field-for-field equality through the wire), plus two
new
TestFuzzCoordinationMessage_MarshalingRoundtrip_With<X>Proposalcrash-safety tests, one per type, matching the existing
one-per-sibling-type convention.
pkg/tbtc/reservation_test.go: renamedTestReservationProposals_UnmarshalRejectsMissingIntegerstoTestReservationProposals_UnmarshalRejectsInvalidFields, matching thebase branch's already-broader field-validation coverage. Added three new
cases proving that a legitimately-constructed proposal with a zero
*big.Intfee/key value is rejected as missing on unmarshal (thedocumented
.Bytes()empty-slice equivalence), exercised through eachproposal's real
Marshal()method rather than a hand-built protobufpayload.
Testing
go test ./pkg/tbtc/...: full package suite passes (362/362), zerofailures.
gofmt -l/go vet: clean on all changed files.Not in this PR
proposals are actually reachable in production - tracked separately.
test - both tracked as separate follow-up PRs per the implementation plan.
Marshal/Unmarshalimplementations, and their required-fieldvalidation) lives on the base branch via fix(spv): re-verify reservation action generation before SPV proof submission #4276, not in this PR's diff;
the milestone row should be closed against that PR, not this one.